﻿// Ttxt2Srt.cpp: implementation of the CTtxt2Srt class.
//
//////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "Ttxt2Srt.h"
#include "UnicodeText.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

CTtxt2Srt::CTtxt2Srt() {}

CTtxt2Srt::~CTtxt2Srt() {}


bool CTtxt2Srt::bConvert(LPCTSTR lpTtxt, LPCTSTR lpSrt)
{
	CUnicodeText ttxt;
	if (!ttxt.bReadTextfile(lpTtxt)) return false;

	CString csTag;
	int iFrom, iStopAt;
	if (!findNextTag(ttxt.tcText, ttxt.lLen, 0, iFrom, iStopAt, csTag)) {
		return false;
	}

	// 맨 앞이 "<?xml "로 시작하는지 확인
	if (csTag.Left(6).Compare(_T("<?xml "))) return false;	// XML이 아님

	int iPos=iStopAt;

	// 다음으로, <TextStream version="1.0"> 찾기
	// 버전 무시하고 "<TextStream "만 찾음

	if (!findTag(ttxt.tcText, ttxt.lLen, iPos, iFrom, iStopAt, _T("TextStream "), csTag)) {
		return false;
	}

	// 부지런히 "<TextSample " 찾아 돌리기
	// 그러면서 "</TextSample>" 찾기

	CUnicodeText srt;
	srt.tcText = new TCHAR[ttxt.lLen*3];
	ZeroMemory(srt.tcText, ttxt.lLen*3);
	srt.lLen=0;
	int iSubCount=0;

	while (true) {
		if (!findTag(ttxt.tcText, ttxt.lLen, iPos, iFrom, iStopAt, _T("TextSample "), csTag)) {
			// 이제 더 할 게 없도다
			break;
		}

		DWORD dwTime1, dwTime2;
		CStringArray csaName, csaValue;
		parseProperties(csTag, csaName, csaValue);
		if (!parseTime(csaName, csaValue, dwTime1)) {
			// 시간 정보가 없는 경우 ㅡ.ㅡ;
			iPos = iStopAt;
			continue;
		}

		int iFrom2, iStopAt2;
		CString csTag2;

		// </TextSample> 찾기
		// 이걸 찾은 뒤에 다음 자막 시간을 찾아야 됨
		if (!findTag(ttxt.tcText, ttxt.lLen, iStopAt, iFrom2, iStopAt2, _T("/TextSample"), csTag2)) {
			// 닫기가 없으면 여기서 포기
			break;
		}

		CString csSubtitle;
		for (int i=iStopAt; i<iFrom2; i++) {
			csSubtitle += ttxt.tcText[i];
		}

		csSubtitle.TrimLeft(_TCHAR('\r'));
		csSubtitle.TrimRight();

		if (csSubtitle.IsEmpty()) {
			// 자막이 없다.
			// 그럼 text property를 확인
			for (int i=0; i<csaName.GetSize(); i++) {
				if (csaName.GetAt(i).Compare(_T("text"))==0) {
					csSubtitle = csaValue.GetAt(i);
					csSubtitle.Replace(_TCHAR('\''), _TCHAR('\r'));
					csSubtitle.TrimLeft();
					csSubtitle.TrimRight();
					while (csSubtitle.Replace(_T("\r\r"), _T("\r")));
				}
			}
		}

		// 여기서 csSubtitle이 비었으면 이건 자막이 아니니 패스
		if (csSubtitle.IsEmpty()) {
			iPos = iStopAt2;
			continue;
		}

		{
			// 다음 자막의 시작 시간 확인
			int iFrom_, iStopAt_;
			CString csTag_;
			if (findTag(ttxt.tcText, ttxt.lLen, iStopAt2, iFrom_, iStopAt_, _T("TextSample "), csTag_)) {
				CStringArray csaName_, csaValue_;
				parseProperties(csTag_, csaName_, csaValue_);
				if (!parseTime(csaName_, csaValue_, dwTime2)) {
					dwTime2 = dwTime1;
				}
			} else {
				dwTime2 = dwTime1;
			}
		}

		iPos = iStopAt2;
		int len = writeTime(srt.tcText+srt.lLen, ++iSubCount, dwTime1, dwTime2);
		srt.lLen+=len;

		fixSubtitle(csSubtitle);

		len = csSubtitle.GetLength();
		memcpy(srt.tcText+srt.lLen, LPCTSTR(csSubtitle), sizeof(TCHAR)*len);
		srt.lLen+=len;
		srt.tcText[srt.lLen++]=_TCHAR('\r');
		srt.tcText[srt.lLen++]=_TCHAR('\r');
	}

	srt.WriteTextfile(lpSrt);

	return true;
}

//리턴 시 iStopAt에는 마지막 태그 다음 위치 저장
bool CTtxt2Srt::findNextTag(TCHAR *tc, int iLen, int iPos, int &iFrom, int &iStopAt, CString &csTag)
{
	if (iPos>=iLen) return false;	// 더 이상 태그 없음
	
	while (iPos<iLen && tc[iPos]!=_TCHAR('<')) iPos++;
	if (iPos>=iLen) return false;	// 더 이상 태그 없음
	iFrom = iPos;

	while (iPos<iLen && tc[iPos]!=_TCHAR('>')) iPos++;
	if (iPos>=iLen) {
		// 닫는 태그가 없는 등신같은 상황에도 꿋꿋이 true 리턴
		iStopAt = iLen;
	} else {
		iStopAt = iPos+1;
	}
	csTag = _T("");
	for (int i=iFrom; i<iStopAt; i++) {
		csTag += tc[i];
	}

	return true;
}

// 특정 태그 찾는 함수, '<'는 빼고 호출
bool CTtxt2Srt::findTag(TCHAR *tc, int iLen, int iPos, int &iFrom, int &iStopAt, LPCTSTR lpTagToFind, CString &csTag)
{
	CString csTagToFind(lpTagToFind);
	csTagToFind = _TCHAR('<')+csTagToFind;
	int lenTagToFind = csTagToFind.GetLength();

	while (true) {
		if (!findNextTag(tc, iLen, iPos, iFrom, iStopAt, csTag)) return false;
		if (csTag.Left(lenTagToFind).Compare(csTagToFind) == 0) return true;

		iPos = iStopAt;
	}
}

// 태그 전체에서 properties만 CStringArray에 저장
void CTtxt2Srt::parseProperties(LPCTSTR lpTag, CStringArray &csaName, CStringArray &csaValue)
{
	CString csTag(lpTag);
	// CUnicodeText에서 엔터는 모두 \r로 통일했음
	// \n은 그냥 혹시나 해서임
	csTag.TrimLeft(_T("< \t\r\n"));
	csTag.TrimRight(_T("/> \t\r\n"));

	csaName.RemoveAll();
	csaValue.RemoveAll();


	lpTag = LPCTSTR(csTag);
	while (*lpTag && *lpTag!=_TCHAR(' ') && *lpTag!=_TCHAR('\t')) lpTag++;
	if (!(*lpTag)) return;

	// 태그의 이름은 확인 완료
	// 이제 attribute의 name과 value를 부지런히 읽어낼 것

	while (true) {
		// 공백에서 시작함
		while ((*lpTag==_TCHAR(' ')) || (*lpTag==_TCHAR('\t'))) lpTag++;
		if (!(*lpTag)) return;

		CString csName;
		while (*lpTag && *lpTag!=_TCHAR(' ') && *lpTag!=_TCHAR('\t') && *lpTag!=_TCHAR('=') && *lpTag!=_TCHAR('\'') && *lpTag!=_TCHAR('\"')) {
			csName+=(*(lpTag++));
		}

		// 이제 '='가 등장할 차례
		// 혹시나 해서 공백 제거. 원래는 있으면 안 됨
		while ((*lpTag==_TCHAR(' ')) || (*lpTag==_TCHAR('\t'))) lpTag++;
		if (*lpTag!=_TCHAR('=')) return;

		lpTag++;
		// 역시, 혹시나 해서 공백 제거. 원래는 있으면 안 됨
		while ((*lpTag==_TCHAR(' ')) || (*lpTag==_TCHAR('\t'))) lpTag++;

		CString csValue;

		switch (*lpTag) {
		case _TCHAR(0):
			// 값이 없어? 포기!
			break;;

		case _TCHAR('\"'):
			// 값을 큰따옴표로 저장
			lpTag++;
			while ((*lpTag) && (*lpTag!=_TCHAR('\"'))) {
				csValue+=(*(lpTag++));
			}
			if (*lpTag==_TCHAR('\"')) lpTag++;

			break;

		case _TCHAR('\''):
			// 값을 작은따옴표로 저장
			lpTag++;
			while ((*lpTag) && (*lpTag!=_TCHAR('\''))) {
				csValue+=(*(lpTag++));
			}
			if (*lpTag==_TCHAR('\'')) lpTag++;
			
			break;

		default:
			// 원래 이래서는 안 되는, 따옴표 없는 경우.
			while ((*lpTag) && (*lpTag!=_TCHAR(' ')) && (*lpTag!=_TCHAR('\t'))) {
				csValue+=(*(lpTag++));
			}
		}

		if (csName.GetLength() && csValue.GetLength()) {
			csaName.Add(csName);
			csaValue.Add(csValue);
		}
	}
}

// 시간을 DWORD 단위로 parse
DWORD CTtxt2Srt::parseTime(LPCTSTR lpTime)
{
	CString csTime;
	while (*lpTime) {
		if (_istdigit(*lpTime)) csTime += (*lpTime);
		lpTime++;
	}
	csTime = (csTime+_T("000000000")).Left(9);
	const int mul[] = {60*60*10000,	60*60*1000,	60*10000,	60*1000,	10000,	1000,	100,	10,	1};

	DWORD ret = 0;
	lpTime = LPCTSTR(csTime);
	for (int i=0; i<9; i++) {
		ret += mul[i]*(lpTime[i]-_TCHAR('0'));
	}

	return ret;
}

bool CTtxt2Srt::parseTime(CStringArray &csaName, CStringArray &csaValue, DWORD &dwMilisec)
{
	int size = csaName.GetSize();
	for (int i=0; i<size; i++) {
		if (csaName.GetAt(i).Compare(_T("sampleTime")) == 0) {
			dwMilisec = parseTime(LPCTSTR(csaValue.GetAt(i)));
			return true;
		}
	}
	return false;
}

int CTtxt2Srt::writeTime(TCHAR *tc, int num, DWORD dw1, DWORD dw2)
{
	int ret;

	CString cs;
	cs.Format(_T("%d"), num);

	ret = cs.GetLength();
	memcpy(tc, LPCTSTR(cs), sizeof(TCHAR)*ret);
	tc[ret++] = _TCHAR('\r');

	// 00:00:00,000 --> 00:00:05,000
	// 012345678901
	TCHAR *tcTime = conv2Time(dw1);
	memcpy(tc+ret, tcTime, sizeof(TCHAR)*12);
	ret+=12;
	
	static TCHAR arrow[] = _T(" --> ");
	memcpy(tc+ret, arrow, sizeof(TCHAR)*5);
	ret+=5;

	tcTime = conv2Time(dw2);
	memcpy(tc+ret, tcTime, sizeof(TCHAR)*12);
	ret+=12;

	tc[ret++] = _TCHAR('\r');

	return ret;
}

TCHAR * CTtxt2Srt::conv2Time(DWORD dw)
{
	// 00:00:00,000
	static TCHAR ret[13];
	DWORD h, m, s, ms;
	ms = dw % 1000;
	s = dw / 1000;
	m = s / 60;
	s %= 60;
	h = m / 60;
	m %= 60;
	if (h>99) h=99;

	_stprintf_s(ret, 13, _T("%02u:%02u:%02u,%03u"), h, m, s, ms);
	return ret;
}

void CTtxt2Srt::fixSubtitle(CString &csSub)
{
	// sub와 style로 구분

	CString sub, style;
	int pos = csSub.Find(_TCHAR('<'));
	if (pos>=0) {
		sub = csSub.Left(pos);
		style = csSub;
		style.Delete(0, pos);
	} else {
		sub = csSub;
	}
	unescapeXML(sub);

	// 스타일을 저장하고 읽기
	// <Style fromChar="7" toChar="26" styles="Italic " fontID="1" fontSize="18" color="ff ff ff ff"/>
	// italic, bold, underlined), text color in RGBA colorspace.
	// 각 글자별로 스타일을 IBUFFFFFF 형식으로 저장
	// 기본값은 ***FFFFFF
	
	CStringArray csaStyles;
	int len = sub.GetLength();
	csaStyles.SetSize(len);
	for (int i=0; i<len; i++) csaStyles.SetAt(i, _T("***FFFFFF"));

	CStringArray csaStyleStr;
	len = style.GetLength();
	int j=0;
	while (j<len) {
		int from = style.Find(_TCHAR('<'), j);
		if (from<0) break;

		int to = style.Find(_TCHAR('>'), from);
		if (to<0) break;

		csaStyleStr.Add(style.Mid(from, to-from+1));
		j = to+1;
	}

	// 스타일을 parsing해서 ***FFFFFF 포맷으로 변경

	len = csaStyleStr.GetSize();
	for (int i=0; i<len; i++) {
		setOneStyle(csaStyles, csaStyleStr.GetAt(i));
	}

	// ***FFFFFF 포맷 스타일을 다시 <b> 형식으로 변경

	j=0;
	len = csaStyles.GetSize();
	csSub = _T("");
	while (j<len) {
		CString now = csaStyles.GetAt(j);
		int p=j;
		while (p<len && now.CompareNoCase(csaStyles.GetAt(p))==0) p++;

		CString open, close;
		if (now.GetAt(0)!=_TCHAR('*')) {
			open+=_T("<i>");
			close=_T("</i>")+close;
		}
		if (now.GetAt(1)!=_TCHAR('*')) {
			open+=_T("<b>");
			close=_T("</b>")+close;
		}
		if (now.GetAt(2)!=_TCHAR('*')) {
			open+=_T("<u>");
			close=_T("</u>")+close;
		}

		CString color = now.Mid(3, 6);
		if (color.CompareNoCase(_T("FFFFFF"))) {
			open+=(_T("<font color=\"#")+color+_T("\">"));
			close=_T("</font>")+close;
		}

		csSub += (open + sub.Mid(j, p-j) + close);
		j=p;
	}
}

void CTtxt2Srt::unescapeXML(CString &cs)
{
	CString cs0;
	int len = cs.GetLength();
	LPCTSTR lp = LPCTSTR(cs);

	int i=0;
	while (i<len) {
		int pos1 = cs.Find(_TCHAR('&'), i);
		if (pos1<0) {
			cs0 += cs.Mid(i, len-i);
			break;
		}

		cs0 += cs.Mid(i, pos1-i);

		int pos2 = cs.Find(_TCHAR(';'), pos1);
		if (pos2<0) {
			break;
		}

		CString xml = cs.Mid(pos1+1, pos2-pos1-1);
		TCHAR tc = unescape(xml);
		if (tc) cs0 += tc;

		i=pos2+1;
	}

	cs = cs0;
}

TCHAR CTtxt2Srt::unescape(CString &cs)
{
	if (cs.IsEmpty()) return 0;
	if (cs.GetAt(0)==_TCHAR('#')) {
		if (cs.GetAt(1)==_TCHAR('x')) {
			return TCHAR(_tcstol(LPCTSTR(cs)+2, NULL, 16));
		} else {
			return TCHAR(_ttoi(LPCTSTR(cs)+1));
		}
	} else {
		static LPCTSTR lpEsc[] = {
			_T("lt"),	_T("gt"),	_T("amp"),	_T("apos"), _T("quot")
		};
		static LPCTSTR tcESC = _T("<>&\'\"");

		for (int i=0; i<sizeof(lpEsc)/sizeof(lpEsc[0]); i++) {
			if (cs.Compare(lpEsc[i]) == 0) {
				return tcESC[i];
			}
		}

		return _TCHAR(' ');
	}
}

void CTtxt2Srt::setOneStyle(CStringArray &csaStyles, LPCTSTR lpOneStyle)
{
	// <Style fromChar="7" toChar="26" styles="Italic " fontID="1" fontSize="18" color="ff ff ff ff"/>
	// italic, bold, underlined), text color in RGBA colorspace.
	// 각 글자별로 스타일을 IBUFFFFFF 형식으로 저장
	// 대문자로 변환해서 처리함

	int len = csaStyles.GetSize();
	CStringArray csaName, csaValue;
	parseProperties(lpOneStyle, csaName, csaValue);
	int fromChar=-1, toChar=-1;
	CString csStyle, csColor;
	
	for (int i=0; i<csaName.GetSize(); i++) {
		CString csName = csaName.GetAt(i);
		CString csValue = csaValue.GetAt(i);

		if (csName.Compare(_T("fromChar")) == 0) {
			fromChar = _ttoi(LPCTSTR(csValue));
		} else if (csName.Compare(_T("toChar")) == 0) {
			toChar = _ttoi(LPCTSTR(csValue));
		} else if (csName.Compare(_T("styles")) == 0) {
			csStyle = csValue;
		} else if (csName.Compare(_T("color")) == 0) {
			csColor = csValue;
		}
	}

	if (fromChar<0 || toChar<0) return;
	if (fromChar>len) fromChar = len;
	if (toChar>len) toChar = len;

	csColor.Replace(_T(" "), NULL);
	csColor.Replace(_T("\t"), NULL);

	if (csColor.GetLength()>6) {
		csColor = csColor.Left(6);
		csColor.MakeUpper();

		for (int i=fromChar; i<toChar; i++) {
			csaStyles.SetAt(i, csaStyles.GetAt(i).Left(3)+csColor);
		}
	}

	if (csStyle.GetLength()) {
		csStyle.MakeLower();
		bool italic = (csStyle.Find(_T("ita"))>=0);
		bool bold = (csStyle.Find(_T("bol"))>=0);
		bool underlined = (csStyle.Find(_T("und"))>=0);

		for (int i=fromChar; i<toChar; i++) {
			CString cs = csaStyles.GetAt(i);
			if (italic) cs.SetAt(0, _TCHAR('I'));
			if (bold) cs.SetAt(1, _TCHAR('B'));
			if (underlined) cs.SetAt(2, _TCHAR('U'));
			csaStyles.SetAt(i, cs);
		}
	}
}
